12 类别

说明:利用Objective-C的动态运行时分配机制,你可以为现有的类添加方法计算属性,这种机制称为类别(category)

  • 可以在类中添加属性(@property),且只能是计算属性(不能添加实例变量
  • 可以向一个添加任意数量的类别
  • 类别可以访问其扩展的类的实例变量

总结:我将类别分3种

种类 @interface @implementation
类别 @interface 类名(类别名) @implementation 类名(类别名)
类扩展(匿名类别) @interface 类名()
前向引导 @interface 类名(类别名) @implementation 类名

12.1 创建类别

说明:可以为人和类添加新的方法,包括那些没有源代码的类。
技巧:通常把类别代码放在独立的文件中,通常以类名称+类别名称的风格命名。

12.1.1 开始创建类别独立文件

说明:使用Xcode往项目中添加类别非常容易,甚至可以类名称+类别名称命名类别文件。

  1. 新建文件
    Alt text
  2. 选择模版
    Alt text
  3. 文件相关
    Alt text
  4. 完成
    Alt text

12.1.2 @interface部分

NSString+NumberConvience.h

1
2
3
4
#import <Foundation/Foundation.h>
@interface NSString (NumberConvience)
- (NSNumber *) lengthAsNumber;
@end// NumberConvience

12.1.3 @implementation部分

NSString+NumberConvience.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import "NSString+NumberConvience.h"

@implementation NSString (NumberConvience)

/**
* 获取NSNumber 类型的字符串长度
* @return {NSNumber *} 字符串长度
*/

- (NSNumber *) lengthAsNumber {
// 获得字符串长度:NSInteger 底层是个 long int
NSUInteger length = [self length];
// 将字符串长度转换为 NSNumber 类型
return ([NSNumber numberWithUnsignedLong: length]);
}// lengthAsNumber

@end

main.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import <Foundation/Foundation.h>
#import "NSString+NumberConvience.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 可变字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
// 添加键/值对
[dict setObject:[@"hello" lengthAsNumber] forKey:@"hello"];
[dict setObject:[@"iLikeFish" lengthAsNumber] forKey:@"iLikeFish"];
[dict setObject:[@"Once upon a time" lengthAsNumber] forKey:@"Once upon a time"];
// 打印字典
NSLog(@"%@", dict);
}
return 0;
}

12.1.4 类别的缺陷

说明:类别有2个局限性

  • 无法向类中添加实例变量
  • 当类别添加的方法和类中原有的方法重名时,类别具有更高的优先级

解决命名冲突:可以在类别的方法名中添加一个前缀,以确保不会发生名称冲突。
添加实例变量:使用全局字典来存储对象与想关联的额外变量之间的映射。

12.1.5 类别的优势

说明:类别主要有3个用途

  • 将类的实现代码分散到多个不同文件或框架中
  • 创建对私有方法的前向引用
  • 向对象添加非正式协议(informal protocol)

12.1.6 类扩展

说名:类扩展(class extension)是一个特殊的类别,它不需要命名(只有@interface没有@implementation)。

  • 可以包含源代码的类中使用
  • 可以添加实例变量
  • 可以将只读权限改成可读写的权限
  • 创建数量不限

信息隐藏:分2种情况

类扩展所在文件 可访问性
扩展的目标类的@implementation所在.m文件 目标类的内部
单独的私有.h文件 目标类的内部、目标类的子类和友类

注意:可以拥有多个类扩展,不过这样会引发很难察觉的bug,所以请理智使用。

Things.h:类的@interface

1
2
3
4
5
6
7
8
9
10
#import <Foundation/Foundation.h>

@interface Things : NSObject

@property (assign) NSInteger thing1;
@property (readonly, assign) NSInteger thing2;

-(void)resetAllValues;

@end

Things.m:类的@implementation和类扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#import "Things.h"

/**
* 在实现文件中添加类扩展(匿名类别)
*/

@interface Things ()
{
// 添加实例变量
NSInteger thing4;
}
// 修改原有属性的特性(只读->读写)
@property (readwrite, assign) NSInteger thing2;
// 添加属性
@property (assign) NSInteger thing3;

@end// Things ()

/**
* 类的原始定义
*/

@implementation Things

- (void) resetAllValues {
thing1 = 100;
thing2 = 200;
}

@end// Things

main.m:使用被扩展后的NSString

1
2
3
4
5
6
7
8
9
10
11
12
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
Things *things = [[Things alloc] init];
things.thing1 = 1;
NSLog(@"%@", things);
[things resetAllValues];
NSLog(@"%@", things);
}
return 0;
}

12.2 利用类别分散实现代码

说明:如果想将大型的单个类分散到多个不同的.m文件中,可以使用类别
举例:AppKit中的NSWindow,拥有大量的类别声明

  • @interface NSWindow(NSKeyboardUI)
  • @interface NSWindow(NSToolbarSupport)
  • @interface NSWindow(NSDrag)
  • @interface NSWindow(NSCarbonExtensions)

扩展:类别还可以将方法分散到逻辑群组中,使编程人员可以更加容易地阅读头文件

在项目中使用类别

说明:将类的类别的实现部分分散在三个独立的文件中。
Alt text

CategoryThing.h:类的@interface和3个类别的@interface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#import <Foundation/Foundation.h>
@interface CategoryThing : NSObject
{
NSInteger thing1;
NSInteger thing2;
NSInteger thing3;
}
@end// CategoryThing

/**
* 类扩展:Thing1
*/

@interface CategoryThing (Thing1)
- (void) setThing1: (NSInteger) thing1;
- (NSInteger) thing1;
@end

/**
* 类扩展:Thing2
*/

@interface CategoryThing (Thing2)
- (void) setThing2: (NSInteger) thing2;
- (NSInteger) thing2;
@end

/**
* 类扩展:Thing3
*/

@interface CategoryThing (Thing3)
- (void) setThing3: (NSInteger) thing3;
- (NSInteger) thing3;
@end

CategoryThing.m:类的@implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import "CategoryThing.h"

@implementation CategoryThing

/**
* 描述
*
* @return 字符串表示的描述内容
*/

- (NSString *) description {
NSString *desc = [NSString stringWithFormat:@"%ld %ld %ld", thing1, thing2, thing3];
return (desc);
}// description

@end// CategoryThing

CategoryThing+Thing1.m:类别Thing1的@implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import "CategoryThing.h"

@implementation CategoryThing (Thing1)
- (void)setThing1:(NSInteger)t1
{
thing1 = t1;
} // setThing1

- (NSInteger) thing1
{
return (thing1);
} // thing1

@end // CategoryThing

CategoryThing+Thing2.m:类别Thing2的@implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
#import "CategoryThing.h"

@implementation CategoryThing (Thing2)

- (void)setThing2:(NSInteger)t2 {
thing2 = t2;
} // setthing2

- (NSInteger) thing2 {
return (thing2);
} // thing2

@end

CategoryThing+Thing3.m:类别Thing3的@implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
#import "CategoryThing.h"

@implementation CategoryThing (Thing3)

- (void)setThing3:(NSInteger)t3 {
thing3 = t3;
} // setthing3

- (NSInteger) thing3 {
return (thing3);
} // thing3

@end

main.m:调用通过类别扩展的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <Foundation/Foundation.h>
#import "CategoryThing.h"

int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建物品集合
CategoryThing *thing = [[CategoryThing alloc] init];
// 调用通过“类别”扩展来的方法
[thing setThing1: 5];
[thing setThing2: 23];
[thing setThing3: 42];

NSLog(@"Things are %@", thing);
}
return 0;
}

12.3 通过类别创建前向引用

背景:Objective-C私有方法分两种

  • 如果在一个类的@implementation部分定义了某个方法,而对应的@interface部分没有相应的方法声明
  • 通过类扩展(匿名类别)扩展的方法

然而,O-C并不真的支持私有方法,所以私有方法仍然可以通过对象调用,只不过这时Xcode会给出警告。
说明:当从外部访问某个私有方法时,为了避免Xcode给出警告,可以通过类别补充一个声明,即前向引导
扩展:实际上,苹果公司官网在知道方针中指出,应用程序不能访问类里面的私有变量和方法,如果你的应用程序有这样的行为,那么苹果公司会拒绝让它上架。
main.m:在最前面创建类别来补充私有方法的声明

1
2
3
4
5
6
@interface Car (Private)

- (void) moveTireFromPosition: (int) pos1 toPosition: (int) pos2;

@end
...

Car.m:方法的实现部分是Car的私有方法

1
2
3
4
5
6
7
@implementation Car
...
- (void) moveTireFromPosition: (int) pos1 toPosition: (int) pos2 {
...
}
...
@end

12.4 非正式协议和委托类别

委托:将某些工作交给另一个类执行就叫做委托(delegate)
委托对象:委托技术中,被委托用来执行某些工作的对象
说明:除了通过继承创建委托对象外,可以通过类别扩展NSObject(即创建了一个非正式协议),使其获得委托方法,从而将任何对象都变成委托对象

12.4.1 iTunesFinder项目

说明:用来说明Cocoa中是如何使用委托技术的。

Bonjour

说明:查找由Bonjour发布的网络服务的Cocoa类NSNetServiceBrowser
用法:告诉网络服务浏览器你需要的服务,并为其提供一个委托对象浏览器对象将会向该委托对象发送消息,告知其发现新服务的时间。
ITunesFinder.h

1
2
3
4
5
#import <Cocoa/Cocoa.h>

@interface ITunesFinder : NSObject <NSNetServiceBrowserDelegate>

@end

ITunesFinder.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#import "ITunesFinder.h"

@implementation ITunesFinder

/**
* 找到了网络服务时的回调方法
*
* @param b 网络服务浏览器对象
* @param service 被发现的服务
* @param moreComing 一批通知是否已经完成的标记
*/

- (void) netServiceBrowser:(NSNetServiceBrowser *) b
didFindService:(NSNetService *) service
moreComing:(BOOL) moreComing {
// 获取关于该服务的所有有趣的属性
[service resolveWithTimeout:10];

NSLog (@"found one! Name is %@", [service name]);

} // didFindService

/**
* 某个网络服务消失时的回调方法(用于状态刷新)
*
* @param b 网络服务浏览器对象
* @param service 被发现的服务
* @param moreComing 一批通知是否已经完成的标记
*/

- (void) netServiceBrowser:(NSNetServiceBrowser *) b
didRemoveService:(NSNetService *) service
moreComing:(BOOL) moreComing
{
[service resolveWithTimeout:10];

NSLog (@"lost one! Name is %@", [service name]);

} // didRemoveService

@end // ITunesFinder

main.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import <Foundation/Foundation.h>
#import "ITunesFinder.h"

int main(int argc, const char * argv[])
{
@autoreleasepool
{
// 创建 网络服务浏览器对象
NSNetServiceBrowser *browser = [[NSNetServiceBrowser alloc] init];
// 创建 委托对象(自定义的查找iTunes资源的对象)
ITunesFinder *finder = [[ITunesFinder alloc] init];

// 通知 网络服务浏览器对象所使用的委托对象为自定义的 ITunes 对象
[browser setDelegate:finder];
// 搜索 iTunes 共享(使用TCP协议、只在本地网络中)
[browser searchForServicesOfType:@"_daap._tcp"
inDomain:@"local."];

NSLog (@"begun browsing");
// run循环(在 网络服务浏览器 发现新的 iTunes 共享之前会一直保持运行而不返回,即阻塞在此处而不执行后面的代码)
[[NSRunLoop currentRunLoop] run];
}
return 0;
}

12.4.2 委托和类别

说明:除了通过继承创建委托对象外,可以通过类别扩展NSObject(即创建了一个非正式协议),使其获得委托方法,从而将任何对象都变成委托对象

1
2
3
4
5
6
7
8
@interface NSObject (NSNetServerBrowserDelegateMethods)

- (void) netServiceBrowserWillSearch: (NSNetServiceBrowser *) browser;
- (void) netServiceBrowser:(NSNetServiceBrowser *) b didFindService:(NSNetService *) service moreComing:(BOOL) moreComing;
- (void) netServiceBrowserDidStopSearch: (NSNetServiceBrowser *) browser;
- (void) netServiceBrowser:(NSNetServiceBrowser *) b didRemoveService:(NSNetService *) service moreComing:(BOOL) moreComing;

@end

12.4.3 响应选择器

说明:NSNetServiceBrowser为了确定其委托对象是否能够处理那些发送给它的消息,会首先检查对象,询问其能否响应该选择器,是泽发送消息,否则忽略这个委托对象,程序继续运行。

选择器(selector)

说明:只是一个方法名称,但以Objective-C运行时使用的特殊方式编码,以快速执行查询。
语法:@selector(方法名)
用途:NSObject提供了一个名为responendsToSelector的方法,该方法询问对象以确定其是否能够响应某个特定的消息。

1
2
3
4
Car *car = [[Car alloc] init];
if ([car responengsToSelector: @selector(setEngine:)]) {
NSLog(@"yowza!");
}

12.4.4 选择器的其它应用

说明:选择器可以

  • 被传递
  • 作为方法的参数
  • 作为实例变量被存储

举个例子:Foundation框架中的NSTimer

12.5 小结